package org.codefilarete.stalactite.spring.repository.query.projection;

import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.codefilarete.reflection.AccessorByMember;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.stalactite.engine.EntityCriteria.OrderByChain.Order;
import org.codefilarete.stalactite.engine.EntityPersister.ExecutableProjectionQuery;
import org.codefilarete.stalactite.engine.ExecutableProjection.ProjectionDataProvider;
import org.codefilarete.stalactite.engine.runtime.projection.ProjectionQueryCriteriaSupport;
import org.codefilarete.stalactite.engine.runtime.projection.ProjectionQueryPageSupport;
import org.codefilarete.stalactite.query.api.JoinLink;
import org.codefilarete.stalactite.query.model.Limit;
import org.codefilarete.stalactite.spring.repository.query.StalactiteQueryMethod;
import org.codefilarete.stalactite.spring.repository.query.execution.AbstractQueryExecutor;
import org.codefilarete.stalactite.spring.repository.query.execution.StalactiteQueryMethodInvocationParameters;
import org.codefilarete.stalactite.sql.result.Accumulator;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.repository.query.Parameter;

import static org.codefilarete.stalactite.spring.repository.query.execution.AbstractRepositoryQuery.buildAliases;

/**
 * Implementation of {@link AbstractQueryExecutor} dedicated to projections.
 * Implementation is based on {@link ProjectionQueryCriteriaSupport}: a copy from the default one is made at construction time to have a dedicated
 * instance for each derived query.
 *
 * @param <C> domain entity type
 * @author Guillaume Mary
 */
class ProjectionQueryExecutor<C> extends AbstractQueryExecutor<List<Object>, Object> {
	
	private final ProjectionQueryCriteriaSupport<C, ?> projectionQueryCriteriaSupport;
	private final Accumulator<ProjectionDataProvider, List<Map<String, Object>>, List<Map<String, Object>>> accumulator;
	
	public ProjectionQueryExecutor(StalactiteQueryMethod method,
								   ProjectionQueryCriteriaSupport<C, ?> defaultProjectionQueryCriteriaSupport,
								   IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties) {
		super(method);
		IdentityHashMap<JoinLink<?, ?>, String> aliases = buildAliases(columnToProperties);
		// we "clone" the default projection query to make our own, dedicated to the derived query
		this.projectionQueryCriteriaSupport = defaultProjectionQueryCriteriaSupport.copyFor(select -> {
			columnToProperties.keySet().forEach(selectable -> {
				select.add(selectable, aliases.get(selectable));
			});
		});
		this.accumulator = new TupleAccumulator(columnToProperties);
	}
	
	@Override
	public Supplier<List<Object>> buildQueryExecutor(StalactiteQueryMethodInvocationParameters invocationParameters) {
		return () -> {
			ExecutableProjectionQuery<C, ?> projectionQuery = handleDynamicParameters(invocationParameters, projectionQueryCriteriaSupport);
			
			int i = 1;
			for (Parameter bindableParameter : invocationParameters.getParameters().getBindableParameters()) {
				Object value = invocationParameters.getBindableValue(bindableParameter.getIndex());
				// Note that we don't apply the value to the criteria of the derived query template, else we temper with it for next execution
				// defaultDerivedQuery.criteriaChain.criteria.get(i).setValue(..);
				projectionQuery.set(String.valueOf(i++), value);
			}
			
			return (List<Object>) (List) projectionQuery.execute(accumulator);
		};
	}
	
	private ExecutableProjectionQuery<C, ?> handleDynamicParameters(StalactiteQueryMethodInvocationParameters invocationParameters,
																	ProjectionQueryCriteriaSupport<C, ?> actualProjectionQueryCriteriaSupport) {
		ProjectionQueryCriteriaSupport<C, ?> derivedQueryToUse;
		// following code will manage both Sort as an argument, and Sort in a Pageable because getSort() handle both
		if (invocationParameters.getSort().isSorted()) {
			Class<?> declaringClass = method.getEntityInformation().getJavaType();
			// Spring Sort class supports only first-level properties, in-depth ones seems not to be definable,
			// therefore we create AccessorChain of only one property
			ProjectionQueryPageSupport<C> dynamicSortSupport = new ProjectionQueryPageSupport<>();
			invocationParameters.getSort().stream().forEachOrdered(order -> {
				AccessorByMember<?, ?, ?> accessor = Accessors.accessor(declaringClass, order.getProperty());
				dynamicSortSupport.orderBy(new AccessorChain<>(accessor),
						order.getDirection() == Direction.ASC ? Order.ASC : Order.DESC,
						order.isIgnoreCase());
			});
			derivedQueryToUse = actualProjectionQueryCriteriaSupport.copyFor(dynamicSortSupport);
		} else {
			derivedQueryToUse = actualProjectionQueryCriteriaSupport;
		}
		ExecutableProjectionQuery<C, ?> result = derivedQueryToUse.wrapIntoExecutable();
		Limit limit = invocationParameters.getLimit();
		if (limit != null) {
			result.limit(limit.getCount(), limit.getOffset());
		}
		return result;
	}
	
	private static class TupleAccumulator<C> implements Accumulator<ProjectionDataProvider, List<Map<String, Object>>, List<Map<String, Object>>> {
		private final IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties;
		
		public TupleAccumulator(IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> columnToProperties) {
			this.columnToProperties = columnToProperties;
		}
		
		@Override
		public Supplier<List<Map<String, Object>>> supplier() {
			return LinkedList::new;
		}
		
		@Override
		public BiConsumer<List<Map<String, Object>>, ProjectionDataProvider> aggregator() {
			return (finalResult, databaseRowDataProvider) -> {
				Map<String, Object> row = new HashMap<>();
				finalResult.add(row);
				for (Entry<JoinLink<?, ?>, AccessorChain<C, ?>> entry : columnToProperties.entrySet()) {
					PartTreeStalactiteProjection.buildHierarchicMap(entry.getValue(), databaseRowDataProvider.getValue(entry.getKey()), row);
				}
			};
		}
		
		@Override
		public Function<List<Map<String, Object>>, List<Map<String, Object>>> finisher() {
			return Function.identity();
		}
	}
}
